/*******************************************************************************
* Copyright (c) 2008, 2012 Stepan Rutz.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stepan Rutz - initial implementation
* Florian Minges - modifier
*******************************************************************************/
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import se.cth.hedgehogphoto.database.LocationObject;
import se.cth.hedgehogphoto.global.Constants;
/**
* MapPanel display tiles from openstreetmap as is. This simple minimal viewer supports zoom around mouse-click center and has a simple api.
* A number of tiles are cached. See {@link #CACHE_SIZE} constant. If you use this it will create traffic on the tileserver you are
* using. Please be conscious about this.
*
* This class is a JPanel which can be integrated into any swing app just by creating an instance and adding like a JLabel.
*
* The map has the size <code>256*1<<zoomlevel</code>. This measure is referred to as map-coordinates. Geometric locations
* like longitude and latitude can be obtained by helper methods. Note that a point in map-coordinates corresponds to a given
* geometric position but also depending on the current zoom level.
*
* You can zoomIn around current mouse position by left double click. Left right click zooms out.
*
* <p>
* Methods of interest are
* <ul>
* <li>{@link #setZoom(int)} which sets the map's zoom level. Values between 1 and 18 are allowed.</li>
* <li>{@link #setMapPosition(Point)} which sets the map's top left corner. (In map coordinates)</li>
* <li>{@link #setCenterPosition(Point)} which sets the map's center position. (In map coordinates)</li>
* <li>{@link #computePosition(java.awt.geom.Point2D.Double)} returns the position in the map panels coordinate system
* for the given longitude and latitude. If you want to center the map around this geometric location you need
* to pass the result to the method</li>
* </ul>
* </p>
*
* <p>As mentioned above Longitude/Latitude functionality is available via the method {@link #computePosition(java.awt.geom.Point2D.Double)}.
* If you have a GIS database you can get this info out of it for a given town/location, invoke {@link #computePosition(java.awt.geom.Point2D.Double)} to
* translate to a position for the given zoom level and center the view around this position using {@link #setCenterPosition(Point)}.
* </p>
*
* <p>The properties <code>zoom</code> and <code>mapPosition</code> are bound and can be tracked via
* regular {@link PropertyChangeListener}s.</p>
*
* <p>License is EPL (Eclipse Public License). Contact at stepan.rutz@gmx.de</p>
*
* @author stepan.rutz
* @modifiedby Florian Minges
*/
@SuppressWarnings("serial")
public class MapPanel extends JPanel {
public static final class TileServer {
private final String url;
private final int maxZoom;
private boolean broken;
private TileServer(String url, int maxZoom) {
this.url = url;
this.maxZoom = maxZoom;
}
public String toString() {
return url;
}
public int getMaxZoom() {
return maxZoom;
}
public String getURL() {
return url;
}
public boolean isBroken() {
return broken;
}
public void setBroken(boolean broken) {
this.broken = broken;
}
}
/* constants ... */
private static final TileServer[] TILESERVERS = {
new TileServer("http://tile.openstreetmap.org/", 18),
/*new TileServer("http://tah.openstreetmap.org/Tiles/tile/", 17),*/ /*This TileServer appears to be broken. */
};
private static final String NAMEFINDER_URL = "http://gazetteer.openstreetmap.org/namefinder/search.xml";
/* The Preferred width and height of the panel. Doesn't change the actual map size, but is important for calculations. */
private static final int PREFERRED_WIDTH = se.cth.hedgehogphoto.global.Constants.PREFERRED_MODULE_WIDTH;
private static final int PREFERRED_HEIGHT = se.cth.hedgehogphoto.global.Constants.PREFERRED_MODULE_HEIGHT;
private static final int ANIMATION_FPS = 15, ANIMATION_DURARTION_MS = 500;
/* TODO: Display the ABOUT_MSG somewhere else in a proper manner. */
private static final int TILE_SIZE = 256;
private static final int CACHE_SIZE = 256;
private static final String ABOUT_MSG =
"MapPanel - Minimal Openstreetmap/Maptile Viewer\r\n" +
"Web: http://mappanel.sourceforge.net\r\n" +
"Written by stepan.rutz. Contact stepan.rutz@gmx.de\r\n\r\n" +
"Tileserver-URLs: " + Arrays.toString(TILESERVERS) + "\r\n" +
"Namefinder-URL: " + NAMEFINDER_URL + "\r\n" +
"Tileserver and Namefinder are part of Openstreetmap or associated projects.\r\n\r\n" +
"MapPanel gets its data from these servers.\r\n\r\n" +
"Please visit and support the actual projects at http://www.openstreetmap.org/.\r\n" +
"And keep in mind this application is just a simple alternative renderer for swing.\r\n";
private static final int MAGNIFIER_SIZE = 100;
//-------------------------------------------------------------------------
// tile url construction.
// change here to support some other tile
public static String getTileString(TileServer tileServer, int xtile, int ytile, int zoom) {
String number = ("" + zoom + "/" + xtile + "/" + ytile);
String url = tileServer.getURL() + number + ".png";
return url;
}
//-------------------------------------------------------------------------
// map impl.
private static Point mapPosition = new Point(0, 0);
protected static int zoom;
private Dimension mapSize = new Dimension(0, 0);
private Point centerPosition = new Point(0, 0);
private TileServer tileServer = TILESERVERS[0];
private DragListener mouseListener = new DragListener();
private TileCache cache = new TileCache();
private ControlPanel controlPanel = new ControlPanel();
private boolean useAnimations = true;
private Animation animation;
protected double smoothScale = 1.0D;
private int smoothOffset = 0;
private Point smoothPosition, smoothPivot;
private Rectangle magnifyRegion;
/** If true, it's possible to move around and scroll in the map.
* If false, it's a static map. */
private boolean interactionEnabled;
public MapPanel() {
this(new Point(8282, 5179), 16);
}
protected MapPanel(Point mapPosition, int zoom) {
try {
// disable animation on windows7 for now
useAnimations = !("Windows Vista".equals(System.getProperty("os.name")) && "6.1".equals(System.getProperty("os.version")));
} catch (Exception e) {
// be defensive here
}
setLayout(new MapLayout());
setOpaque(true);
setBackground(new Color(0xc0, 0xc0, 0xc0));
add(controlPanel);
addMouseListener(mouseListener);
addMouseMotionListener(mouseListener);
addMouseWheelListener(mouseListener);
setZoom(zoom);
updateMapPositionWithoutFire(mapPosition.x, mapPosition.y);
checkTileServers();
checkActiveTileServer();
interactionEnabled = false; //will prevent listener to work correctly
}
public void addObserver(PropertyChangeListener listener) {
addPropertyChangeListener(listener);
}
/*---------------------------------------------------------------------
HERE COMES SOME IMPORTANT CODE FOR OURS IMPLEMENTATION OF THE MAPPANEL@author Florian
---------------------------------------------------------------------*/
private List<LocationObject> locations;
private Point.Double centerLocation;
public void calibrate(List<LocationObject> locations) {
setBounds(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT);
this.locations = locations;
updateCenterLocation();
adjustZoom();
enableControlPanel(true);
enableInteraction(true);
setOpaque(true);
}
/** Calculates the proper zoom so that every location fits on the
* visible part of the map. */
private void adjustZoom() {
if (centerLocation.getX() == 0.0 && centerLocation.getY() == 0.0) {
setZoom(1);
centerMap();
return;
}
int zoom = 16;
setZoom(zoom);
centerMap();
while (!allLocationsVisible() && zoom != 1) {
setZoom(--zoom);
centerMap();
}
}
/** Internal method for updating the centerLocation-variable. */
private void updateCenterLocation() {
Point.Double location = new Point.Double();
location.setLocation(averageLongitude(), averageLatitude());
centerLocation = location;
}
/** Tells the map to center its' view to the specified centerLocation. */
private void centerMap() {
Point position = computeMapPosition(centerLocation.getX(), centerLocation.getY());
setCenterPosition(position);
}
/** Returns true if all Locations are visible on the map. */
private boolean allLocationsVisible() {
boolean result = true;
int nbrOfLocations = locations.size();
for (int index = 0; index < nbrOfLocations; index++) {
Point pixelPosition = getPixelPosition(locations.get(index));
if (!validPixelPosition(pixelPosition)) {
result = false;
break;
}
}
return result;
}
/** Returns true if the passed pixel p is: 0 < p < SIZE.
* ie if it is part of the map. Returns false otherwise. */
private boolean validPixelPosition(Point pixelPosition) {
boolean longitudeOK = (pixelPosition.x > Constants.PREFERRED_MODULE_WIDTH * 0.05 && pixelPosition.x < Constants.PREFERRED_MODULE_WIDTH * 0.95);
boolean latitudeOK = (pixelPosition.y > Constants.PREFERRED_MODULE_HEIGHT * 0.10 && pixelPosition.y < Constants.PREFERRED_MODULE_HEIGHT * 0.90);
return (longitudeOK && latitudeOK);
}
/** Returns the average Latitude for the stored locations.
* If there are no locations, 0.0 is returned. */
private double averageLatitude() {
double totalLatitude = 0.0;
double nbrOfLocations = locations.size();
for(int i = 0; i < nbrOfLocations; i++) {
totalLatitude += locations.get(i).getLatitude();
}
double averageLatitude = nbrOfLocations != 0 ? totalLatitude / nbrOfLocations : 0.0;
return averageLatitude;
}
/** Returns the average Longitude for the stored locations.
* If there are no locations, 0.0 is returned. */
private double averageLongitude() {
double totalLongitude = 0.0;
double nbrOfLocations = locations.size();
for(int i = 0; i < nbrOfLocations; i++) {
totalLongitude += locations.get(i).getLongitude();
}
double averageLongitude = nbrOfLocations != 0 ? totalLongitude / nbrOfLocations : 0.0;
return averageLongitude;
}
/** Returns a list of pixel coordinates for all Locations.
* These pixel coordinates specify where, relative to the map,
* where the locationMarkers have to be placed. */
public Point getPixelPosition(LocationObject location) {
Point pixelPosition = computeMapPosition(location);
pixelPosition.x = pixelPosition.x - getMapPosition().x;
pixelPosition.y = pixelPosition.y - getMapPosition().y;
return pixelPosition;
}
public static Point computePixelPositionOnMap(double longitude, double latitude) {
Point p = computeMapPosition(longitude, latitude);
p.x -= mapPosition.x;
p.y -= mapPosition.y;
return p;
}
private static Point computeMapPosition(LocationObject location) {
double longitude = location.getLongitude();
double latitude = location.getLatitude();
return computeMapPosition(longitude, latitude);
}
private static Point computeMapPosition(double longitude, double latitude) {
return computePosition(new Point2D.Double(longitude, latitude));
}
/*---------------------------------------------------------------------
HERE ENDS SOME IMPORTANT CODE
---------------------------------------------------------------------*/
@SuppressWarnings("unused")
private void checkTileServers() {
for (TileServer tileServer : TILESERVERS) {
String urlstring = getTileString(tileServer, 1, 1, 1);
try {
URL url = new URL(urlstring);
Object content = url.getContent();
} catch (Exception e) {
tileServer.setBroken(true);
}
}
}
private void checkActiveTileServer() {
/* IF POSSIBLE: Make it only check if the tileServer != null
* if that is the case, call checkTileServers and see if it is broken. */
if (getTileServer() != null && getTileServer().isBroken()) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
String error = "The tileserver \"" + getTileServer().getURL() + "\" could not be reached.\r\nMaybe configuring a http-proxy is required.";
JOptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor(MapPanel.this),
error, "TileServer not reachable.", JOptionPane.ERROR_MESSAGE);
}
});
}
}
public void nextTileServer() {
int index = Arrays.asList(TILESERVERS).indexOf(getTileServer());
if (index == -1)
return;
setTileServer(TILESERVERS[(index + 1) % TILESERVERS.length]);
repaint();
}
public TileServer getTileServer() {
return tileServer;
}
public void setTileServer(TileServer tileServer) {
if(this.tileServer == tileServer)
return;
this.tileServer = tileServer;
while (getZoom() > tileServer.getMaxZoom())
zoomOut(new Point(getWidth() / 2, getHeight() / 2));
checkActiveTileServer();
}
public boolean isUseAnimations() {
return useAnimations;
}
public void setUseAnimations(boolean useAnimations) {
this.useAnimations = useAnimations;
}
public ControlPanel getControlPanel() {
return controlPanel;
}
public TileCache getCache() {
return cache;
}
public Point getMapPosition() {
return new Point(mapPosition.x, mapPosition.y);
}
public void setMapPosition(Point mapPosition) {
setMapPosition(mapPosition.x, mapPosition.y);
}
public void setMapPosition(int x, int y) {
if (mapPosition.x == x && mapPosition.y == y)
return;
Point oldValue = new Point(mapPosition.x, mapPosition.y);
updateMapPositionWithoutFire(x, y);
firePropertyChange("mapPosition", oldValue, mapPosition);
}
public void updateMapPositionWithoutFire(int x, int y) {
if (mapPosition.x == x && mapPosition.y == y)
return;
mapPosition.x = x;
mapPosition.y = y;
centerPosition.x = mapPosition.x + PREFERRED_WIDTH / 2;
centerPosition.y = mapPosition.y + PREFERRED_HEIGHT / 2;
}
public void translateMapPosition(int tx, int ty) {
setMapPosition(mapPosition.x + tx, mapPosition.y + ty);
}
protected int getZoom() {
return zoom;
}
protected void setZoom(int zoom) {
if (zoom == MapPanel.zoom)
return;
MapPanel.zoom = Math.min(getTileServer().getMaxZoom(), zoom);
mapSize.width = getXMax();
mapSize.height = getYMax();
}
/** Enables/disables map interaction
* If set to false, it becomes a static map. */
public void enableInteraction(boolean state) {
interactionEnabled = state;
}
/** Hides/shows overlay and control panel */
public void enableAllOverlayPanels(boolean state) {
enableControlPanel(state);
}
/** Doesn't actually enable/disable the panel,
* just hides or shows it. */
public void enableControlPanel(boolean state) {
getControlPanel().setVisible(state);
}
public void zoomInAnimated(Point pivot) {
if (!useAnimations) {
zoomIn(pivot);
return;
}
if (animation != null)
return;
mouseListener.downCoords = null;
animation = new Animation(AnimationType.ZOOM_IN, ANIMATION_FPS, ANIMATION_DURARTION_MS) {
protected void onComplete() {
smoothScale = 1.0d;
smoothPosition = smoothPivot = null;
smoothOffset = 0;
animation = null;
repaint();
}
protected void onFrame() {
smoothScale = 1.0 + getFactor();
repaint();
}
};
smoothPosition = new Point(mapPosition.x, mapPosition.y);
smoothPivot = new Point(pivot.x, pivot.y);
smoothOffset = -1;
zoomIn(pivot);
animation.run();
}
public void zoomOutAnimated(Point pivot) {
if (!useAnimations) {
zoomOut(pivot);
return;
}
if (animation != null)
return;
mouseListener.downCoords = null;
animation = new Animation(AnimationType.ZOOM_OUT, ANIMATION_FPS, ANIMATION_DURARTION_MS) {
protected void onComplete() {
smoothScale = 1.0d;
smoothPosition = smoothPivot = null;
smoothOffset = 0;
animation = null;
repaint();
}
protected void onFrame() {
smoothScale = 1 - .5 * getFactor();
repaint();
}
};
smoothPosition = new Point(mapPosition.x, mapPosition.y);
smoothPivot = new Point(pivot.x, pivot.y);
smoothOffset = 1;
zoomOut(pivot);
animation.run();
}
public void zoomIn(Point pivot) {
if (getZoom() >= getTileServer().getMaxZoom())
return;
Dimension oldValue = new Dimension(mapSize.width, mapSize.height);
int dx = pivot.x - PREFERRED_WIDTH / 2;
int dy = pivot.y - PREFERRED_WIDTH / 2;
Point mapPosition = getMapPosition();
setZoom(getZoom() + 1);
updateMapPositionWithoutFire(mapPosition.x * 2 + dx + PREFERRED_WIDTH / 2,
mapPosition.y * 2 + dy + PREFERRED_HEIGHT / 2); //zooms in so that the cursor stays were it was (points to the same location)
firePropertyChange(Global.ZOOM_IN_EVENT, oldValue, mapSize);
repaint();
}
public void zoomOut(Point pivot) {
if (getZoom() <= 1)
return;
Dimension oldValue = new Dimension(mapSize.width, mapSize.height);
int dx = pivot.x;
int dy = pivot.y;
Point mapPosition = getMapPosition();
setZoom(getZoom() - 1);
updateMapPositionWithoutFire((mapPosition.x - dx) / 2,
(mapPosition.y - dy) / 2); //zooms out so that the cursor stays were it was (points to the same location)
firePropertyChange(Global.ZOOM_OUT_EVENT, oldValue, mapSize);
repaint();
}
public int getXTileCount() {
return (1 << zoom);
}
public int getYTileCount() {
return (1 << zoom);
}
public int getXMax() {
return TILE_SIZE * getXTileCount();
}
public int getYMax() {
return TILE_SIZE * getYTileCount();
}
public Point getCursorPosition() {
return new Point(mapPosition.x + mouseListener.mouseCoords.x, mapPosition.y + mouseListener.mouseCoords.y);
}
public Point getTile(Point position) {
return new Point((int) Math.floor(((double) position.x) / TILE_SIZE),(int) Math.floor(((double) position.y) / TILE_SIZE));
}
public Point getCenterPosition() {
if (getWidth() != 0 && getHeight() != 0)
return new Point(mapPosition.x + getWidth() / 2, mapPosition.y + getHeight() / 2);
return new Point(mapPosition.x + PREFERRED_WIDTH / 2, mapPosition.y + PREFERRED_HEIGHT / 2);
}
public Point getStoredCenterPosition() {
return centerPosition;
}
public void setCenterPosition(Point p) {
setMapPosition(p.x - PREFERRED_WIDTH / 2, p.y - PREFERRED_HEIGHT / 2);
centerPosition = p;
}
public Point.Double getLongitudeLatitude(Point position) {
return new Point.Double(
position2lon(position.x),
position2lat(position.y));
}
public static Point computePosition(Point.Double coords) {
int x = lon2position(coords.x);
int y = lat2position(coords.y);
return new Point(x, y);
}
protected void paintComponent(Graphics gOrig) {
super.paintComponent(gOrig);
Graphics2D g = (Graphics2D) gOrig.create();
try {
paintInternal(g);
} finally {
g.dispose();
}
}
/** Important inner class which should NOT be ported to the outside. */
private static final class Painter {
private final int zoom;
private float transparency = 1F;
private double scale = 1d;
private final MapPanel mapPanel;
private Painter(MapPanel mapPanel, int zoom) {
this.mapPanel = mapPanel;
this.zoom = zoom;
}
public float getTransparency() {
return transparency;
}
public void setTransparency(float transparency) {
this.transparency = transparency;
}
public double getScale() {
return scale;
}
public void setScale(double scale) {
this.scale = scale;
}
private void paint(Graphics2D gOrig, Point mapPosition, Point scalePosition) {
Graphics2D g = (Graphics2D) gOrig.create();
try {
if (getTransparency() < 1f && getTransparency() >= 0f) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, transparency));
}
if (getScale() != 1d) {
AffineTransform xform = new AffineTransform();
xform.translate(scalePosition.x, scalePosition.y);
xform.scale(scale, scale);
xform.translate(-scalePosition.x, -scalePosition.y);
g.transform(xform);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
int width = mapPanel.getWidth();
int height = mapPanel.getHeight();
int x0 = (int) Math.floor(((double) mapPosition.x) / TILE_SIZE);
int y0 = (int) Math.floor(((double) mapPosition.y) / TILE_SIZE);
int x1 = (int) Math.ceil(((double) mapPosition.x + width) / TILE_SIZE);
int y1 = (int) Math.ceil(((double) mapPosition.y + height) / TILE_SIZE);
int dy = y0 * TILE_SIZE - mapPosition.y;
for (int y = y0; y < y1; ++y) {
int dx = x0 * TILE_SIZE - mapPosition.x;
for (int x = x0; x < x1; ++x) {
paintTile(g, dx, dy, x, y);
dx += TILE_SIZE;
}
dy += TILE_SIZE;
}
if (getScale() == 1d && mapPanel.magnifyRegion != null) {
Rectangle magnifyRegion = new Rectangle(mapPanel.magnifyRegion);
magnifyRegion.translate(-mapPosition.x, -mapPosition.y);
g.setColor(Color.yellow);
}
} finally {
g.dispose();
}
}
private void paintTile(Graphics2D g, int dx, int dy, int x, int y) {
boolean DEBUG = false;
boolean DRAW_IMAGES = true;
boolean DRAW_OUT_OF_BOUNDS = false;
boolean imageDrawn = false;
int xTileCount = 1 << zoom;
int yTileCount = 1 << zoom;
boolean tileInBounds = x >= 0 && x < xTileCount && y >= 0 && y < yTileCount;
boolean drawImage = DRAW_IMAGES && tileInBounds;
if (drawImage) {
TileCache cache = mapPanel.getCache();
TileServer tileServer = mapPanel.getTileServer();
Image image = cache.get(tileServer, x, y, zoom);
if (image == null) {
final String url = getTileString(tileServer, x, y, zoom);
try {
image = Toolkit.getDefaultToolkit().getImage(new URL(url));
} catch (Exception e) {
}
if (image != null)
cache.put(tileServer, x, y, zoom, image);
}
if (image != null) {
g.drawImage(image, dx, dy, mapPanel);
imageDrawn = true;
}
}
if (DEBUG && (!imageDrawn && (tileInBounds || DRAW_OUT_OF_BOUNDS))) {
g.setColor(Color.blue);
g.fillRect(dx + 4, dy + 4, TILE_SIZE - 8, TILE_SIZE - 8);
g.setColor(Color.gray);
String s = "T " + x + ", " + y + (!tileInBounds ? " #" : "");
g.drawString(s, dx + 4+ 8, dy + 4 + 12);
}
}
}
private void paintInternal(Graphics2D g) {
if (smoothPosition != null) {
{
Point position = getMapPosition();
Painter painter = new Painter(this, getZoom());
painter.paint(g, position, null);
}
Point position = new Point(smoothPosition.x, smoothPosition.y);
Painter painter = new Painter(this, getZoom() + smoothOffset);
painter.setScale(smoothScale);
float t = (float) (animation == null ? 1f : 1 - animation.getFactor());
painter.setTransparency(t);
painter.paint(g, position, smoothPivot);
if (animation != null && animation.getType() == AnimationType.ZOOM_IN) {
int cx = smoothPivot.x, cy = smoothPivot.y;
drawScaledRect(g, cx, cy, animation.getFactor(), 1 + animation.getFactor());
} else if (animation != null && animation.getType() == AnimationType.ZOOM_OUT) {
int cx = smoothPivot.x, cy = smoothPivot.y;
drawScaledRect(g, cx, cy, animation.getFactor(), 2 - animation.getFactor());
}
}
if (smoothPosition == null) {
Point position = getMapPosition();
Painter painter = new Painter(this, getZoom());
painter.paint(g, position, null);
}
/** DO NOT REMOVE THIS CASE
* Since width/height of the panel initially is 0 before its painted,
* it appears to have the size "0". What was thought to be the center,
* actually becomes the top left corner of the map. This case fixes that problem.
* Though one should care for the possibility of an eternity loop as the maps
* width and height are REALLY 0. This is also handled by checking if size is legit.
**/
if (centerPosition.equals(mapPosition) && legitMapSize()) {
setCenterPosition(centerPosition);
paintInternal(g); /* calls itself, DANGEROUS. Could end up in eternal loop if setCenterPosition is buggy. */
}
}
private boolean legitMapSize() {
return (PREFERRED_WIDTH != 0 && PREFERRED_HEIGHT != 0);
}
private void drawScaledRect(Graphics2D g, int cx, int cy, double f, double scale) {
AffineTransform oldTransform = g.getTransform();
g.translate(cx, cy);
g.scale(scale, scale);
g.translate(-cx, -cy);
int c = 0x80 + (int) Math.floor(f * 0x60);
if (c < 0) c = 0;
else if (c > 255) c = 255;
Color color = new Color(c, c, c);
g.setColor(color);
g.drawRect(cx - 40, cy - 30, 80, 60);
g.setTransform(oldTransform);
}
//-------------------------------------------------------------------------
// utils
// TODO: The way of using static zoom and mapPosition-variables was probably a bad idea.
// It will do for v1.0, but should get a more object-oriented design, so that one can
// instantiate several maps if one wants to. This will however, result in a minor
// architecture-design-rebuild.
public static String format(double d) {
return String.format("%.5f", d);
}
public static double getN(int y, int z) {
double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z);
return n;
}
public static double position2lon(int x, int z) {
double xmax = TILE_SIZE * (1 << z);
return x / xmax * 360.0 - 180;
}
public static double position2lat(int y, int z) {
double ymax = TILE_SIZE * (1 << z);
return Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * y) / ymax)));
}
public double position2lon(int x) {
return position2lon(x, zoom);
}
public double position2lat(int y) {
return position2lat(y, zoom);
}
public static double tile2lon(int x, int z) {
return x / Math.pow(2.0, z) * 360.0 - 180;
}
public static double tile2lat(int y, int z) {
return Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z))));
}
public static int lon2position(double lon, int z) {
double xmax = TILE_SIZE * (1 << z);
return (int) Math.floor((lon + 180) / 360 * xmax);
}
public static int lat2position(double lat, int z) {
double ymax = TILE_SIZE * (1 << z);
return (int) Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * ymax);
}
public static int lon2position(double lon) {
return lon2position(lon, zoom);
}
public static int lat2position(double lat) {
return lat2position(lat, zoom);
}
public static String getTileNumber(TileServer tileServer, double lat, double lon, int zoom) {
int xtile = (int) Math.floor((lon + 180) / 360 * (1 << zoom));
int ytile = (int) Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1 << zoom));
return getTileString(tileServer, xtile, ytile, zoom);
}
private static void drawBackground(Graphics2D g, int width, int height) {
Color color1 = Color.black;
Color color2 = new Color(0x30, 0x30, 0x30);
color1 = new Color(0xc0, 0xc0, 0xc0);
color2 = new Color(0xe0, 0xe0, 0xe0);
Composite oldComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.75f));
g.setPaint(new GradientPaint(0, 0, color1, 0, height, color2));
g.fillRoundRect(0, 0, width, height, 4, 4);
g.setComposite(oldComposite);
}
private static void drawRollover(Graphics2D g, int width, int height) {
Color color1 = Color.white;
Color color2 = new Color(0xc0, 0xc0, 0xc0);
Composite oldComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.25f));
g.setPaint(new GradientPaint(0, 0, color1, width, height, color2));
g.fillRoundRect(0, 0, width, height, 4, 4);
g.setComposite(oldComposite);
}
private static BufferedImage flip(BufferedImage image, boolean horizontal, boolean vertical) {
int width = image.getWidth(), height = image.getHeight();
if (horizontal) {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width / 2; ++x) {
int tmp = image.getRGB(x, y);
image.setRGB(x, y, image.getRGB(width - 1 - x, y));
image.setRGB(width - 1 - x, y, tmp);
}
}
}
if (vertical) {
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height / 2; ++y) {
int tmp = image.getRGB(x, y);
image.setRGB(x, y, image.getRGB(x, height - 1 - y));
image.setRGB(x, height - 1 - y, tmp);
}
}
}
return image;
}
private static BufferedImage makeIcon(Color background) {
final int WIDTH = 16, HEIGHT = 16;
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < HEIGHT; ++y)
for (int x = 0; x < WIDTH; ++x)
image.setRGB(x, y, 0);
Graphics2D g2d = (Graphics2D) image.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(background);
g2d.fillOval(0, 0, WIDTH - 1, HEIGHT - 1);
double hx = 4;
double hy = 4;
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
double dx = x - hx;
double dy = y - hy;
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist > WIDTH) {
dist = WIDTH;
}
int color = image.getRGB(x, y);
int a = (color >>> 24) & 0xff;
int r = (color >>> 16) & 0xff;
int g = (color >>> 8) & 0xff;
int b = (color >>> 0) & 0xff;
double coef = 0.7 - 0.7 * dist / WIDTH;
image.setRGB(x, y, (a << 24) | ((int) (r + coef * (255 - r)) << 16) | ((int) (g + coef * (255 - g)) << 8) | (int) (b + coef * (255 - b)));
}
}
g2d.setColor(Color.gray);
g2d.drawOval(0, 0, WIDTH - 1, HEIGHT - 1);
return image;
}
private static BufferedImage makeXArrow(Color background) {
BufferedImage image = makeIcon(background);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fillPolygon(new int[] { 10, 4, 10} , new int[] { 5, 8, 11 }, 3);
image.flush();
return image;
}
private static BufferedImage makeYArrow(Color background) {
BufferedImage image = makeIcon(background);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fillPolygon(new int[] { 5, 8, 11} , new int[] { 10, 4, 10 }, 3);
image.flush();
return image;
}
private static BufferedImage makePlus(Color background) {
BufferedImage image = makeIcon(background);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fillRect(4, 7, 8, 2);
g.fillRect(7, 4, 2, 8);
image.flush();
return image;
}
private static BufferedImage makeMinus(Color background) {
BufferedImage image = makeIcon(background);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fillRect(4, 7, 8, 2);
image.flush();
return image;
}
//-------------------------------------------------------------------------
// helpers
private enum AnimationType {
ZOOM_IN, ZOOM_OUT
}
private static abstract class Animation implements ActionListener {
private final AnimationType type;
private final Timer timer;
private long t0 = -1L;
private long dt;
private final long duration;
public Animation(AnimationType type, int fps, long duration) {
this.type = type;
this.duration = duration;
int delay = 1000 / fps;
timer = new Timer(delay, this);
timer.setCoalesce(true);
timer.setInitialDelay(0);
}
public AnimationType getType() {
return type;
}
protected abstract void onComplete();
protected abstract void onFrame();
public double getFactor() {
return (double) getDt() / getDuration();
}
public void actionPerformed(ActionEvent e) {
if (getDt() >= duration) {
kill();
onComplete();
return;
}
onFrame();
}
public long getDuration() {
return duration;
}
public long getDt() {
if (!timer.isRunning())
return dt;
long now = System.currentTimeMillis();
if (t0 < 0)
t0 = now;
return now - t0 + dt;
}
public void run() {
if (timer.isRunning())
return;
timer.start();
}
public void kill() {
if (!timer.isRunning())
return;
dt = getDt();
timer.stop();
}
}
private static class Tile {
private final String key;
public final int x, y, z;
public Tile(String tileServer, int x, int y, int z) {
this.key = tileServer;
this.x = x;
this.y = y;
this.z = z;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
result = prime * result + x;
result = prime * result + y;
result = prime * result + z;
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Tile other = (Tile) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
if (x != other.x)
return false;
if (y != other.y)
return false;
if (z != other.z)
return false;
return true;
}
}
private static class TileCache {
private LinkedHashMap<Tile,Image> map = new LinkedHashMap<Tile,Image>(CACHE_SIZE, 0.75f, true) {
protected boolean removeEldestEntry(java.util.Map.Entry<Tile,Image> eldest) {
boolean remove = size() > CACHE_SIZE;
return remove;
}
};
public void put(TileServer tileServer, int x, int y, int z, Image image) {
map.put(new Tile(tileServer.getURL(), x, y, z), image);
}
public Image get(TileServer tileServer, int x, int y, int z) {
Image image = map.get(new Tile(tileServer.getURL(), x, y, z));
return image;
}
@SuppressWarnings("unused")
public int getSize() {
return map.size();
}
}
public static class CustomSplitPane extends JComponent {
private static final int SPACER_SIZE = 4;
private final boolean horizonal;
private final JComponent spacer;
private double split = 0.5;
private int dx, dy;
private Component componentOne, componentTwo;
public CustomSplitPane(boolean horizonal) {
this.spacer = new JPanel();
this.spacer.setOpaque(false);
this.spacer.setCursor(horizonal ? Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR) : Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
this.dx = this.dy = -1;
this.horizonal = horizonal;
/* because of jdk1.5, javafx */
class SpacerMouseAdapter extends MouseAdapter implements MouseMotionListener {
public void mouseReleased(MouseEvent e) {
Insets insets = getInsets();
int width = getWidth();
int height = getHeight();
int availw = width - insets.left - insets.right;
int availh = height - insets.top - insets.bottom;
if (CustomSplitPane.this.horizonal && dy != -1) {
setSplit((double) dx / availw);
} else if (dx != -1) {
setSplit((double) dy / availh);
}
dx = dy = -1;
spacer.setOpaque(false);
repaint();
}
public void mouseDragged(MouseEvent e) {
dx = e.getX() + spacer.getX();
dy = e.getY() + spacer.getY();
spacer.setOpaque(true);
if (dx != -1 && CustomSplitPane.this.horizonal) {
spacer.setBounds(dx, 0, SPACER_SIZE, getHeight());
} else if (dy != -1 && !CustomSplitPane.this.horizonal) {
spacer.setBounds(0, dy, getWidth(), SPACER_SIZE);
}
repaint();
}
public void mouseMoved(MouseEvent e) {
}
};
SpacerMouseAdapter mouseAdapter = new SpacerMouseAdapter();
spacer.addMouseListener(mouseAdapter);
spacer.addMouseMotionListener(mouseAdapter);
setLayout(new LayoutManager() {
public void addLayoutComponent(String name, Component comp) {
}
public void removeLayoutComponent(Component comp) {
}
public Dimension minimumLayoutSize(Container parent) {
return new Dimension(1, 1);
}
public Dimension preferredLayoutSize(Container parent) {
return new Dimension(128, 128);
}
public void layoutContainer(Container parent) {
Insets insets = parent.getInsets();
int width = parent.getWidth();
int height = parent.getHeight();
int availw = width - insets.left - insets.right;
int availh = height - insets.top - insets.bottom;
if (CustomSplitPane.this.horizonal) {
availw -= SPACER_SIZE;
int width1 = Math.max(0, (int) Math.floor(split * availw));
int width2 = Math.max(0, availw - width1);
if (componentOne.isVisible() && !componentTwo.isVisible()) {
spacer.setBounds(0, 0, 0, 0);
componentOne.setBounds(insets.left, insets.top, availw, availh);
} else if (!componentOne.isVisible() && componentTwo.isVisible()) {
spacer.setBounds(0, 0, 0, 0);
componentTwo.setBounds(insets.left, insets.top, availw, availh);
} else {
spacer.setBounds(insets.left + width1, insets.top, SPACER_SIZE, availh);
componentOne.setBounds(insets.left, insets.top, width1, availh);
componentTwo.setBounds(insets.left + width1 + SPACER_SIZE, insets.top, width2, availh);
}
} else {
availh -= SPACER_SIZE;
int height1 = Math.max(0, (int) Math.floor(split * availh));
int height2 = Math.max(0, availh - height1);
if (componentOne.isVisible() && !componentTwo.isVisible()) {
spacer.setBounds(0, 0, 0, 0);
componentOne.setBounds(insets.left, insets.top, availw, availh);
} else if (!componentOne.isVisible() && componentTwo.isVisible()) {
spacer.setBounds(0, 0, 0, 0);
componentTwo.setBounds(insets.left, insets.top, availw, availh);
} else {
spacer.setBounds(insets.left, insets.top + height1, availw, SPACER_SIZE);
componentOne.setBounds(insets.left, insets.top, availw, height1);
componentTwo.setBounds(insets.left, insets.top + height1 + SPACER_SIZE, availw, height2);
}
}
}
});
add(spacer);
}
public double getSplit() {
return split;
}
public void setSplit(double split) {
if (split < 0)
split = 0;
else if (split > 1)
split = 1;
this.split = split;
invalidate();
validate();
}
public void setComponentOne(Component component) {
this.componentOne = component;
if (componentOne != null)
add(componentOne);
}
public void setComponentTwo(Component component) {
this.componentTwo = component;
if (componentTwo != null)
add(componentTwo);
}
}
private class DragListener extends MouseAdapter implements MouseMotionListener, MouseWheelListener {
private Point mouseCoords;
private Point downCoords;
private Point downPosition;
public DragListener() {
mouseCoords = new Point();
}
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() >= 2) {
zoomInAnimated(new Point(mouseCoords.x, mouseCoords.y));
} else if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() >= 2) {
zoomOutAnimated(new Point(mouseCoords.x, mouseCoords.y));
} else if (e.getButton() == MouseEvent.BUTTON2) {
setCenterPosition(getCursorPosition());
repaint();
}
}
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
downCoords = e.getPoint();
downPosition = getMapPosition();
} else if (e.getButton() == MouseEvent.BUTTON3) {
int cx = getCursorPosition().x;
int cy = getCursorPosition().y;
magnifyRegion = new Rectangle(cx - MAGNIFIER_SIZE / 2, cy - MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE, MAGNIFIER_SIZE);
repaint();
}
}
public void mouseReleased(MouseEvent e) {
handleDrag(e);
downCoords = null;
downPosition = null;
magnifyRegion = null;
}
public void mouseMoved(MouseEvent e) {
handlePosition(e);
}
public void mouseDragged(MouseEvent e) {
handlePosition(e);
handleDrag(e);
}
public void mouseExited(MouseEvent e) {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
public void mouseEntered(MouseEvent me) {
super.mouseEntered(me);
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
private void handlePosition(MouseEvent e) {
mouseCoords = e.getPoint();
MapPanel.this.repaint();
}
private void handleDrag(MouseEvent e) {
if (!interactionEnabled) {
/* interaction disabled */
} else if (downCoords != null) {
int tx = downCoords.x - e.getX();
int ty = downCoords.y - e.getY();
setMapPosition(downPosition.x + tx, downPosition.y + ty);
repaint();
} else if (magnifyRegion != null) {
int cx = getCursorPosition().x;
int cy = getCursorPosition().y;
magnifyRegion = new Rectangle(cx - MAGNIFIER_SIZE / 2, cy - MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE, MAGNIFIER_SIZE);
repaint();
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
int rotation = e.getWheelRotation();
if (!interactionEnabled) {
/* interaction disabled */
} else if (rotation < 0) {
zoomInAnimated(new Point(mouseCoords.x, mouseCoords.y));
} else {
zoomOutAnimated(new Point(mouseCoords.x, mouseCoords.y));
}
}
}
public final class ControlPanel extends JPanel {
protected static final int MOVE_STEP = 32;
private JButton makeButton(Action action) {
JButton b = new JButton(action);
b.setFocusable(false);
b.setText(null);
b.setContentAreaFilled(false);
b.setBorder(BorderFactory.createEmptyBorder());
BufferedImage image = (BufferedImage) ((ImageIcon)b.getIcon()).getImage();
BufferedImage hl = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) hl.getGraphics();
g.drawImage(image, 0, 0, null);
drawRollover(g, hl.getWidth(), hl.getHeight());
hl.flush();
b.setRolloverIcon(new ImageIcon(hl));
return b;
}
public ControlPanel() {
setOpaque(false);
setForeground(Color.white);
setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
setLayout(new BorderLayout());
Action zoomInAction = new AbstractAction() {
{
String text = "Zoom In";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makePlus(new Color(0xc0, 0xc0, 0xc0)), false, false)));
}
public void actionPerformed(ActionEvent e) {
zoomInAnimated(new Point(MapPanel.this.getWidth() / 2, MapPanel.this.getHeight() / 2));
}
};
Action zoomOutAction = new AbstractAction() {
{
String text = "Zoom Out";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makeMinus(new Color(0xc0, 0xc0, 0xc0)), false, false)));
}
public void actionPerformed(ActionEvent e) {
zoomOutAnimated(new Point(MapPanel.this.getWidth() / 2, MapPanel.this.getHeight() / 2));
}
};
Action upAction = new AbstractAction() {
{
String text = "Up";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makeYArrow(new Color(0xc0, 0xc0, 0xc0)), false, false)));
}
public void actionPerformed(ActionEvent e) {
translateMapPosition(0, -MOVE_STEP);
MapPanel.this.repaint();
}
};
Action downAction = new AbstractAction() {
{
String text = "Down";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makeYArrow(new Color(0xc0, 0xc0, 0xc0)), false, true)));
}
public void actionPerformed(ActionEvent e) {
translateMapPosition(0, +MOVE_STEP);
MapPanel.this.repaint();
}
};
Action leftAction = new AbstractAction() {
{
String text = "Left";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makeXArrow(new Color(0xc0, 0xc0, 0xc0)), false, false)));
}
public void actionPerformed(ActionEvent e) {
translateMapPosition(-MOVE_STEP, 0);
MapPanel.this.repaint();
}
};
Action rightAction = new AbstractAction() {
{
String text = "Right";
putValue(Action.NAME, text);
putValue(Action.SHORT_DESCRIPTION, text);
putValue(Action.SMALL_ICON, new ImageIcon(flip(makeXArrow(new Color(0xc0, 0xc0, 0xc0)), true, false)));
}
public void actionPerformed(ActionEvent e) {
translateMapPosition(+MOVE_STEP, 0);
MapPanel.this.repaint();
}
};
JPanel moves = new JPanel(new BorderLayout());
moves.setOpaque(false);
JPanel zooms = new JPanel(new BorderLayout(0, 0));
zooms.setOpaque(false);
zooms.setBorder(BorderFactory.createEmptyBorder(3, 0, 0, 0));
moves.add(makeButton(upAction), BorderLayout.NORTH);
moves.add(makeButton(leftAction), BorderLayout.WEST);
moves.add(makeButton(downAction), BorderLayout.SOUTH);
moves.add(makeButton(rightAction), BorderLayout.EAST);
zooms.add(makeButton(zoomInAction), BorderLayout.NORTH);
zooms.add(makeButton(zoomOutAction), BorderLayout.SOUTH);
add(moves, BorderLayout.NORTH);
add(zooms, BorderLayout.SOUTH);
}
public void paint(Graphics gOrig) {
Graphics2D g = (Graphics2D) gOrig.create();
try {
int w = getWidth(), h = getHeight();
drawBackground(g, w, h);
} finally {
g.dispose();
}
super.paint(gOrig);
}
}
private final class MapLayout implements LayoutManager {
public void addLayoutComponent(String name, Component comp) {
}
public void removeLayoutComponent(Component comp) {
}
public Dimension minimumLayoutSize(Container parent) {
return new Dimension(1, 1);
}
public Dimension preferredLayoutSize(Container parent) {
return new Dimension(PREFERRED_WIDTH, PREFERRED_HEIGHT);
}
public void layoutContainer(Container parent) {
{
Dimension psize = controlPanel.getPreferredSize();
controlPanel.setBounds(20, 20, psize.width, psize.height);
}
}
}
}